#Region "Using Directives"

Imports System
Imports System.Text

#End Region

Namespace C4F.DevKit.Bluetooth

    Namespace ServiceAndDeviceDiscovery

        ''' <summary>
        ''' Represents the Bluetooth device and contains methods, events and 
        ''' properties which are required for communicating with remote 
        ''' device.
        ''' </summary>        
        Public NotInheritable Class Device

#Region "Private Members"

            ''' <summary>
            ''' Represents the address of remote device.
            ''' </summary>
            Private m_address As BluetoothSocketAddress

            ''' <summary>
            ''' Represents the type of Bluetooth device.
            ''' </summary>
            Private m_type As DeviceType

            ''' <summary>
            ''' Represents the name of the remote device.
            ''' </summary>
            Private m_name As String

            ''' <summary>
            ''' Represents the queryset which contains device-specific information.
            ''' </summary>
            Private wsaQuerySet As QuerySet

#End Region

#Region "Constructors"

            ''' <summary>
            ''' Initializes a device object.
            ''' </summary>
            ''' <param name="querySet">Contains device-specific information.
            ''' </param>       
            Friend Sub New(ByVal querySet As QuerySet)

                With Me
                    ' Store the queryset in a local variable.
                    .wsaQuerySet = querySet

                    ' Extract device address.
                    .m_address = GetAddress(wsaQuerySet)

                    ' Extract device name.
                    .m_name = GetName(wsaQuerySet)

                    ' Extract device type.
                    .m_type = [GetType](wsaQuerySet)
                End With
            End Sub

#End Region

#Region "Internal Functions"

            ''' <summary>
            ''' Discovers devices based on the service type provided.
            ''' </summary>
            ''' <param name="type">Represents the type of service to be 
            ''' discovered.</param>
            ''' <returns>List of RemoteServices which are of the specified type.
            ''' </returns>
            ''' <exception cref="WinsockException">Thrown when 
            ''' when a windows socket error occurs.</exception> 
            Friend Function DiscoverServicesByType(ByVal type As ServiceType) _
                            As System.Collections.Generic.List(Of RemoteService)

                ' Discover all Services on the remote device.
                Dim remServices As System.Collections.Generic.List(Of RemoteService) = DiscoverAllServices()
                Dim remoteServices As New System.Collections.Generic.List(Of RemoteService)()

                ' If service type matches that of the specified type then 
                ' return the remote service.
                If remServices IsNot Nothing AndAlso remServices.Count <> 0 Then
                    For Each remServ As RemoteService In remServices
                        If remServ.Type = type Then
                            remoteServices.Add(remServ)
                        End If
                    Next
                End If

                ' Return the remoteServices list.
                Return remoteServices

            End Function

            ''' <summary>
            ''' Discovers services based on the service id provided where the 
            ''' serviceId is in Guid format.
            ''' </summary>
            ''' <param name="id">Represents the service id in Guid format.</param>
            ''' <returns>Remote Service whose id matches that of specified id.
            ''' </returns>        
            ''' <exception cref="WinsockException">Thrown when 
            ''' when a windows socket error occurs.</exception> 
            Friend Function DiscoverServiceById(ByVal id As Guid) As RemoteService

                ' Discover all Services on the remote device.
                Dim remoteServices As System.Collections.Generic.List(Of RemoteService) = DiscoverAllServices()

                Dim remoteService As RemoteService = Nothing

                ' If service id matches that of the specified id then return the 
                ' remote service.
                If remoteServices IsNot Nothing AndAlso remoteServices.Count <> 0 Then
                    For Each remServ As RemoteService In remoteServices
                        If remServ.Id.ServiceIdType = ServiceIdType.TypeUuid Then
                            If remServ.Id.ServiceIdUnion.Uuid128Value = id Then
                                remoteService = remServ
                                Exit For
                            End If
                        End If
                    Next
                End If
                Return remoteService

            End Function


            ''' <summary>
            ''' Discovers services based on the service id provided where the 
            ''' service id is in ushort format.
            ''' </summary>
            ''' <param name="id">Specifies the service id in ushort format.</param>
            ''' <returns>Remote Service whose id matches that of specified id.
            ''' </returns> 
            ''' <exception cref="WinsockException">Thrown when 
            ''' when a windows socket error occurs.</exception> 
            Friend Function DiscoverServiceById(ByVal id As UShort) As RemoteService

                ' Discover all Services on the remote device.
                Dim remoteServices As System.Collections.Generic.List(Of RemoteService) = DiscoverAllServices()

                Dim remoteService As RemoteService = Nothing

                ' If service id matches that of the specified id then return the 
                ' remote service.
                If remoteServices IsNot Nothing AndAlso remoteServices.Count <> 0 Then
                    For Each remServ As RemoteService In remoteServices
                        If remServ.Id.ServiceIdType = ServiceIdType.TypeUint16 Then
                            If remServ.Id.ServiceIdUnion.Uuid16Value = id Then
                                remoteService = remServ
                                Exit For
                            End If
                        End If
                    Next
                End If
                Return remoteService

            End Function

            ''' <summary>
            ''' Discovers services based on the service id provided where 
            ''' the id is in uint format.
            ''' </summary>
            ''' <param name="id">Represents the service id in uint format
            ''' .</param>
            ''' <returns>Remote Service whose id matches that of specified id.
            ''' </returns>
            ''' <exception cref="WinsockException">Thrown when 
            ''' when a windows socket error occurs.</exception> 
            Friend Function DiscoverServiceById(ByVal id As UInteger) As RemoteService

                ' Discover all Services on the remote device.
                Dim remoteServices As System.Collections.Generic.List(Of RemoteService) = DiscoverAllServices()

                Dim remoteService As RemoteService = Nothing

                ' If service id matches that of the specified id then return the 
                ' remote service.
                If remoteServices IsNot Nothing AndAlso remoteServices.Count <> 0 Then
                    For Each remServ As RemoteService In remoteServices
                        If remServ.Id.ServiceIdType = ServiceIdType.TypeUint32 Then
                            If remServ.Id.ServiceIdUnion.Uuid32Value = id Then
                                remoteService = remServ
                                Exit For
                            End If
                        End If
                    Next
                End If
                Return remoteService

            End Function

#End Region

#Region "Private Functions"

            ''' <summary>
            ''' Extracts the device address from the Query set structure.
            ''' </summary>
            ''' <param name="querySet">Contains device-specific information.
            ''' </param>
            ''' <returns>Device address of the remote device.</returns>
            Private Function GetAddress(ByVal querySet As QuerySet) As BluetoothSocketAddress

                Try
                    ' Verify if a device address can be extracted.
                    If querySet.NumberOfCsAddrs > 0 Then

                        Dim csAddrInfo As New CsAddrInfo()
                        System.Runtime.InteropServices.Marshal.PtrToStructure(querySet.Buffer, csAddrInfo)

                        ' Get the remote address structure.
                        Dim addr As New BluetoothSocketAddress()
                        System.Runtime.InteropServices.Marshal.PtrToStructure( _
                                            csAddrInfo.RemoteAddr.Sockaddress, _
                                            addr)

                        ' Get the addres family.
                        addr.AddressFamily = CUShort(System.Runtime.InteropServices.Marshal.ReadInt16( _
                                                        csAddrInfo.RemoteAddr.Sockaddress, 0))

                        ' Get the remote address.
                        addr.Address = CULng(System.Runtime.InteropServices.Marshal.ReadInt64( _
                                                csAddrInfo.RemoteAddr.Sockaddress, _
                                                BluetoothHelper.ADDRESS_OFFSET))

                        ' Get the port.
                        addr.Port = CUInt(System.Runtime.InteropServices.Marshal.ReadInt32( _
                                            csAddrInfo.RemoteAddr.Sockaddress, _
                                            BluetoothHelper.CHANNEL_OFFSET))

                        addr.ServiceClassId = _
                                        CType(System.Runtime.InteropServices.Marshal.PtrToStructure(CType( _
                                            (csAddrInfo.RemoteAddr.Sockaddress.ToInt64() + BluetoothHelper.GUID_OFFSET),  _
                                            IntPtr), GetType(Guid)), Guid)

                        Return addr
                    Else
                        Return Nothing
                    End If
                Catch
                    Return Nothing
                End Try

            End Function

            ''' <summary>
            ''' Extracts the device type from the Query set structure.
            ''' </summary>
            ''' <param name="querySet">Contains device-specific information.
            ''' </param>
            ''' <returns>Device Type of remote device.</returns>
            Private Overloads Function [GetType](ByVal querySet As QuerySet) As DeviceType

                Try
                    Dim serviceClassId As Integer = System.Runtime.InteropServices.Marshal.ReadInt32( _
                                                                querySet.ServiceClassId)
                    Dim serviceClassIdValue As String = [String].Format("{0:x2}", serviceClassId)

                    ' The service class id is in the format 0xSSDDdd where SS 
                    ' represents for Major Service class,DD represents major 
                    ' device class and dd represents minor device class.
                    ' Only DDdd is accessed here.

                    Dim deviceClassId As String = serviceClassIdValue.Substring(2)

                    Dim hexValue As Integer = Integer.Parse( _
                                                    deviceClassId, _
                                                    System.Globalization.NumberStyles.HexNumber)

                    m_type = CType(hexValue, DeviceType)
                    Return m_type
                Catch
                    Return DeviceType.UnclassifiedDevice
                End Try

            End Function

            ''' <summary>
            ''' Extracts the device name from the Query set structure.
            ''' </summary>
            ''' <param name="querySet">Contains device-specific information.
            ''' </param>
            ''' <returns>Returns the name of remote device.</returns>
            Private Function GetName(ByVal querySet As QuerySet) As String

                ' Name of the device.
                Try
                    Dim name As String = ""
                    ' If ServiceInstanceName is empty then return the device address
                    ' in string format.
                    If querySet.ServiceInstanceName.Length = 0 Then
                        Try
                            Dim csAddrInfo As New CsAddrInfo()
                            System.Runtime.InteropServices.Marshal.PtrToStructure(querySet.Buffer, csAddrInfo)

                            Dim addressAsString As IntPtr = _
                                        System.Runtime.InteropServices.Marshal.AllocHGlobal(1000)

                            Dim addressSize As Integer = 4096

                            Dim result As Integer = BluetoothHelper.WSAAddressToString( _
                                                        csAddrInfo.RemoteAddr.Sockaddress, _
                                                        csAddrInfo.RemoteAddr.SockaddressLength, _
                                                        Nothing, addressAsString, addressSize)

                            BluetoothHelper.GetError()
                            Dim addressString As String = _
                                        System.Runtime.InteropServices.Marshal.PtrToStringAnsi(addressAsString)

                            System.Runtime.InteropServices.Marshal.FreeHGlobal(addressAsString)
                            name = addressString
                        Catch
                            name = m_address.Address.ToString()
                        End Try
                    Else
                        name = RegularExpressions.Regex.Replace( _
                                        querySet.ServiceInstanceName, _
                                        SDPMessages.PatternForNonPrintChars, _
                                        String.Empty, _
                                        RegularExpressions.RegexOptions.IgnoreCase)
                    End If

                    Return name
                Catch
                    Return Nothing
                End Try

            End Function

            ''' <summary>
            ''' Discovers all services advertisized by the device.
            ''' </summary>
            ''' <returns>Returns a list of services on the device.</returns>
            ''' <exception cref="WinsockException">Thrown when 
            ''' when a windows socket error occurs.</exception> 
            Private Function DiscoverAllServices() As System.Collections.Generic.List(Of RemoteService)

                Dim result As Integer = 0
                Dim remoteServices As New System.Collections.Generic.List(Of RemoteService)()

                ' Initialize WinSock.
                Dim wsadata As New WsaData()

                result = BluetoothHelper.WSAStartup(BluetoothHelper.MakeWord(2, 2), wsadata)

                If result <> 0 Then
                    BluetoothHelper.GetError()
                End If

                ' Initialize the queryset structure with information required 
                ' to query for services

                Dim wsaq As New QuerySet()
                wsaq.Size = System.Runtime.InteropServices.Marshal.SizeOf(GetType(QuerySet))
                wsaq.NameSpace = BluetoothHelper.NS_BTH

                Dim csAddrInfo As New CsAddrInfo()
                System.Runtime.InteropServices.Marshal.PtrToStructure(wsaQuerySet.Buffer, csAddrInfo)

                Dim addressAsString As IntPtr = _
                                System.Runtime.InteropServices.Marshal.AllocHGlobal(1000)
                Dim addressSize As Integer = 4096

                ' Convert address to string format.
                result = BluetoothHelper.WSAAddressToString( _
                                csAddrInfo.RemoteAddr.Sockaddress, _
                                csAddrInfo.RemoteAddr.SockaddressLength, _
                                Nothing, addressAsString, addressSize)

                If result <> 0 Then
                    BluetoothHelper.GetError()
                End If

                Dim addressString As String = _
                System.Runtime.InteropServices.Marshal.PtrToStringAnsi(addressAsString)

                wsaq.Context = addressString
                System.Runtime.InteropServices.Marshal.FreeHGlobal(addressAsString)

                ' Set the service class id to public browse root.
                Dim serviceGuid As Guid = BluetoothHelper.PUBLIC_BROWSE_UUID
                Dim ptrServiceClassId As IntPtr = _
                                System.Runtime.InteropServices.Marshal.AllocHGlobal( _
                                    System.Runtime.InteropServices.Marshal.SizeOf(serviceGuid))
                System.Runtime.InteropServices.Marshal.StructureToPtr(serviceGuid, ptrServiceClassId, True)

                ' Assign service guid to be searched.
                wsaq.ServiceClassId = ptrServiceClassId

                Dim hLookup As IntPtr = IntPtr.Zero
                ' Set the flags appropriate for search.
                Dim dwFlags As UInteger = _
                        BluetoothHelper.LUP_FLUSHCACHE Or _
                        BluetoothHelper.LUP_RETURN_NAME Or _
                        BluetoothHelper.LUP_RETURN_TYPE Or _
                        BluetoothHelper.LUP_RETURN_ADDR Or _
                        BluetoothHelper.LUP_RETURN_BLOB Or _
                        BluetoothHelper.LUP_RETURN_COMMENT

                ' Initiates a client query that is constrained by the 
                ' information contained within a queryset structure.

                result = BluetoothHelper.WSALookupServiceBegin(wsaq, dwFlags, hLookup)
                If result <> 0 Then
                    BluetoothHelper.GetError()
                End If

                While 0 = result
                    Dim dwBuffer As Integer = 65536
                    Dim pBuffer As IntPtr = System.Runtime.InteropServices.Marshal.AllocHGlobal(dwBuffer)
                    Dim qsResult As New QuerySet()

                    ' Retrieves the requested service information.
                    result = BluetoothHelper.WSALookupServiceNext(hLookup, dwFlags, dwBuffer, pBuffer)
                    If 0 = result Then
                        System.Runtime.InteropServices.Marshal.PtrToStructure(pBuffer, qsResult)
                        Dim remoteService As New RemoteService(qsResult, Me.m_address)
                        ' Add the remote service to list.   
                        remoteServices.Add(remoteService)
                    Else
                        ' Get the winsock error.
                        BluetoothHelper.GetError()

                        ' Free unmanaged resources.
                        System.Runtime.InteropServices.Marshal.FreeHGlobal(ptrServiceClassId)
                    End If
                    ' Free unmanaged resources.
                    System.Runtime.InteropServices.Marshal.FreeHGlobal(pBuffer)
                End While

                ' End service-lookup.
                result = BluetoothHelper.WSALookupServiceEnd(hLookup)
                If result <> 0 Then
                    BluetoothHelper.GetError()
                End If

                ' Cleanup winsock.
                BluetoothHelper.WSACleanup()

                If result <> 0 Then
                    BluetoothHelper.GetError()
                End If

                ' Return remote services.
                Return remoteServices

            End Function

#End Region

#Region "Public Properties"

            ''' <summary>
            ''' Gets the type of device.
            ''' </summary>
            Public ReadOnly Property Type() As DeviceType

                Get
                    Return Me.m_type
                End Get

            End Property

            ''' <summary>
            ''' Gets the name of the device.
            ''' </summary>
            Public ReadOnly Property Name() As String

                Get
                    Return Me.m_name
                End Get

            End Property

            ''' <summary>
            ''' Gets the address of the device.
            ''' </summary>
            Public ReadOnly Property Address() As BluetoothSocketAddress

                Get
                    Return Me.m_address
                End Get

            End Property

            ''' <summary>
            ''' Gets a list of services on the device.
            ''' </summary>
            Public ReadOnly Property Services() As System.Collections.Generic.List(Of RemoteService)

                Get
                    Return DiscoverAllServices()
                End Get

            End Property

#End Region

        End Class

    End Namespace

End Namespace

